package com.eatme.web.app.coupon.distribution.service.impl;

import com.alibaba.fastjson2.JSON;
import com.eatme.web.app.coupon.common.constant.Constant;
import com.eatme.web.app.coupon.common.exception.AppException;
import com.eatme.web.app.coupon.distribution.constant.CouponStatus;
import com.eatme.web.app.coupon.distribution.entity.Coupon;
import com.eatme.web.app.coupon.distribution.service.IRedisService;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.RandomUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * Redis 相关的操作服务接口实现
 */
@Service
public class RedisServiceImpl implements IRedisService {
    private static final Logger LOG = LoggerFactory.getLogger(RedisServiceImpl.class);

    /** Redis 客户端 */
    private final StringRedisTemplate redisTemplate;

    public RedisServiceImpl(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public List<Coupon> getCacheCoupons(Long userId, Integer status) {
        LOG.info("Get CouponS From Cache:{}, {}", userId,status);
        String redisKey = status2RedisKey(status,userId);
        List<String> couponStrs = redisTemplate.opsForHash().values(redisKey).stream()
                .map(o->Objects.toString(o,null))
                .collect(Collectors.toList());

        if (CollectionUtils.isEmpty(couponStrs)) {
            saveEmptyCouponListToCache(userId,
                    Collections.singletonList(status));
            return Collections.emptyList();
        }

        return couponStrs.stream().map(cs -> JSON.parseObject(cs,Coupon.class)).collect(Collectors.toList());
    }

    /**
     * 保存空的优惠券列表到缓存中
     * 目的:  避免缓存穿透
     * @param userId 用户id
     * @param status 优惠券状态列表
     */
    @Override
    public void saveEmptyCouponListToCache(Long userId, List<Integer> status) {

        LOG.info("Save Empty List To Cache For User: {}, Status: {}",userId, JSON.toJSONString(status));

        // key 是coupon_id,value 是序列化的Coupon
        Map<String,String> ivalidCouponMap = new HashMap<>();
        ivalidCouponMap.put("-1",JSON.toJSONString(Coupon.invalidCoupon()));

        // 用户优惠券缓存信息
        //KV
        // K: status -> redisKey
        // V: {coupon_id,序列化Coupon }

        // 使用 SessionCallback 把数据命令放入到 Redis 的 pipeline
        SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                status.forEach(s->{
                    String redisKey = status2RedisKey(s,userId);
                    operations.opsForHash().putAll( redisKey,ivalidCouponMap);
                });
                return null;
            }
        };

        LOG.info("Pipeline Exe Result: {}",JSON.toJSONString(redisTemplate.executePipelined(sessionCallback)));

    }

    /**
     * 尝试从cache 中获取一个优惠券码
     * @param templateId 优惠券模板主键
     * @return 优惠券码
     */
    @Override
    public String tryToAcquireCouponCodeFromCache(Integer templateId) {

        String redisKey = String.format("%s%s", Constant.RedisPrefix.COUPON_TEMPLATE,templateId.toString());

        // 因为优惠券码不存在顺序关系,左边 pop 或者右边 pop, 没有影响
        String couponCode = redisTemplate.opsForList().leftPop(redisKey);

        LOG.info("Acquire Coupon Code: {], {], {}",templateId,redisKey,couponCode);

        return couponCode;
    }

    @Override
    public Integer addCouponToCache(Long userId,
                                    List<Coupon> coupons,
                                    Integer status) throws AppException {

        LOG.info("add Coupon To Cache: {}, {}, {}",userId, JSON.toJSONString(coupons),status);

        Integer result = -1;
        CouponStatus couponStatus  = CouponStatus.of(status);

        switch (couponStatus) {
            case USABLE:
                result = addCouponToCacheForUsable(userId,coupons);
                break;
            case USED:
                result = addCouponToCacheForUsed(userId,coupons);
                break;
            case EXPIRED:
                result = addCouponToCacheForExpired(userId,coupons);
                break;
        }
        return result;
    }

    /**
     * 新增加优惠券到 Cache 中
     * @param userId
     * @param coupons
     * @return
     */
    private Integer addCouponToCacheForUsable(Long userId,List<Coupon> coupons) {

        // 如果 status 是USABLE,代表是新增加的优惠券
        // 只会影响一个 Cache: USER_COUPON_USABLE
        LOG.debug("Add Coupon TO Cache For Usable");
        Map<String,String> needCacheObject = new HashMap<>();
        coupons.forEach(c ->
            needCacheObject.put(
                    c.getId().toString(),
                    JSON.toJSONString(c)));
        String redisKey = status2RedisKey(
                CouponStatus.USABLE.getCode(),userId);
        redisTemplate.opsForHash().putAll(redisKey,needCacheObject);
        LOG.info("Add {} Coupons To Cache: {}, {}",needCacheObject.size(),userId,redisKey);

        redisTemplate.expire(redisKey,getRandomExpirationTime(1,2), TimeUnit.SECONDS);

        return needCacheObject.size();
    }

    /**
     * 将已使用的优惠券加入到 Cache 中
     * @param userId
     * @param coupons
     * @return
     * @throws AppException
     */
    private Integer addCouponToCacheForUsed(Long userId,List<Coupon> coupons)throws AppException {

        // 如果 status 是 USED,代表用户操作是使用当前的优惠券,影响到两个Cache
        // USABLE, USED
        LOG.debug("Add Coupon TO Cache For Used");
        Map<String,String> needCacheForUsed = new HashMap<>(coupons.size());
        String redisKeyForUsable = status2RedisKey(
                CouponStatus.USABLE.getCode(),userId);
        String redisKeyForUsed = status2RedisKey(
                CouponStatus.USED.getCode(),userId);

        // 获取当前用户可用的优惠券
        List<Coupon> curUsableCoupons = getCacheCoupons(
                userId,CouponStatus.USED.getCode());

        // 当前可用的优惠券个数一定是大于1的
        assert curUsableCoupons.size() > coupons.size();
        coupons.forEach(coupon -> needCacheForUsed.put(
                coupon.getId().toString(),
                JSON.toJSONString(coupon)
        ));
        // 检验当前的优惠券参数是否与Cached 中的匹配
        List<Integer> curUsableIds = curUsableCoupons.stream()
                .map(Coupon::getId).collect(Collectors.toList());
        List<Integer> paramIds = coupons.stream()
                .map(Coupon::getId).collect(Collectors.toList());

        if (!CollectionUtils.isSubCollection(paramIds,curUsableIds)) {
            LOG.error("CurCoupons Is Not Equal ToCache: {}, {}, {}",
                    userId,
                    JSON.toJSONString(curUsableIds,
                    JSON.toJSONString(paramIds)
                    ));
            throw new AppException("CurCoupons Is Not Equal To Cache!");
        }

        List<String> needCleanKey = paramIds.stream().
                map(i->i.toString()).collect(Collectors.toList());
        SessionCallback<Object>  sessionCallback = new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {

                // 1. 已使用的优惠券 Cache 缓存
                operations.opsForHash().putAll(redisKeyForUsed,needCacheForUsed
                );
                // 2. 可用的优惠券 Cache 需要清理
                operations.opsForHash().delete(redisKeyForUsable,needCleanKey.toArray()
                );
                // 3. 重置过期时间
                operations.expire(
                        redisKeyForUsable,
                        getRandomExpirationTime(1,2),
                        TimeUnit.SECONDS
                );
                operations.expire(
                        redisKeyForUsed,
                        getRandomExpirationTime(1,2),
                        TimeUnit.SECONDS
                );
                return null;
            }
        };
        LOG.info("Pipeline Exe Result: {}",JSON.toJSONString(redisTemplate.executePipelined(sessionCallback)));
        return coupons.size();
    }

    /**
     * 将过期优惠券加入到Cache 中
     * @param userId
     * @param coupons
     * @return
     */
    private Integer addCouponToCacheForExpired(Long userId,List<Coupon> coupons) throws AppException {

        // status 是 EXPIRED, 代表是已有的优惠券过期了,影响到两个 Cache
        // USABLE, EXPIRED
        LOG.debug("Add Coupon To Cache For Expired.");

        // 最终需要保存的Cahce
        Map<String,String> needCachedForExpired = new HashMap<>(coupons.size());
        String redisKeyForUsable = status2RedisKey(
                CouponStatus.USABLE.getCode(),
                userId
        );
        String redisKeyForExpired = status2RedisKey(
                CouponStatus.EXPIRED.getCode(),
                userId
        );

        List<Coupon> curUsableCoupons = getCacheCoupons(
                userId,CouponStatus.USED.getCode());
        List<Coupon> curExpiredCoupons = getCacheCoupons(
                userId,CouponStatus.EXPIRED.getCode()
        );

        // 当前可用的优惠券个数一定是大于1的
        assert curUsableCoupons.size() > coupons.size();

        coupons.forEach(coupon -> needCachedForExpired.put(
                coupon.getId().toString(),
                JSON.toJSONString(coupon)
        ));

        // 校验当前优惠券参数是否与 Cache 中的匹配
        List<Integer> curUsableIds = curUsableCoupons.stream()
                .map(Coupon::getId).collect(Collectors.toList());
        List<Integer> paramIds = coupons.stream()
                .map(Coupon::getId).collect(Collectors.toList());
        if (!CollectionUtils.isSubCollection(paramIds,curUsableIds)){
            LOG.info("Coupons Is Not Equal To Cache: {}, {}, {}",
                    userId,JSON.toJSONString(curUsableIds),
                    JSON.toJSONString(paramIds));
            throw  new AppException("CurCoupon Is Not Equal To Cache.");
        }

        List<String> needCleanKey = paramIds.stream()
                .map(integer -> integer.toString()).collect(Collectors.toList());
        SessionCallback<Object> sessionCallback = new SessionCallback<Object>() {
            @Override
            public  Object execute(RedisOperations operations) throws DataAccessException {

                // 已过期的优惠券 Cache 缓存
                operations.opsForHash().putAll(
                        redisKeyForExpired,needCachedForExpired);
                // 可用的优惠券 Cache 需要清理
                operations.opsForHash().delete(
                        redisKeyForUsable,needCleanKey.toArray()
                );
                // 3. 重置过期时间
                operations.expire(redisKeyForUsable,
                        getRandomExpirationTime(1,2),TimeUnit.SECONDS);
                operations.expire(redisKeyForExpired,
                        getRandomExpirationTime(1,2),TimeUnit.SECONDS);
                return null;
            }
        };
        LOG.info("Pipline Exe Result: {]",
                JSON.toJSONString(redisTemplate.executePipelined(sessionCallback)));

        return coupons.size();
    }

    /**
     * 根据status 获取到对应的Redis Key
     * @param status
     * @param userId
     * @return
     */
    private String status2RedisKey(Integer status,Long userId) {
        String redisKey = null;
        CouponStatus couponStatus = CouponStatus.of(status);
        switch (couponStatus) {
            case USABLE:
                redisKey =String.format("%s%s", Constant.RedisPrefix.USER_COUPON_USABLE,userId);
                break;
            case USED:
                redisKey =String.format("%s%s", Constant.RedisPrefix.USER_COUPON_USED,userId);
                break;
            case EXPIRED:
                redisKey =String.format("%s%s", Constant.RedisPrefix.USER_COUPON_EXPIRED,userId);
                break;
        }
        return redisKey;
    }

    /**
     * 获取一个随机的过期时间
     * 缓存雪崩: key 在同一时间失效
     * @param min 最小的时数
     * @param max 最大的时数
     * @return 返回 [min,max] 之间的随机秒数
     */
    private Long getRandomExpirationTime(Integer min,Integer max) {

        return RandomUtils.nextLong(
                min * 60 * 60,
                max * 60 * 60
        );
    }
}
